/****************************************************************************/
/*  File:       Repository.java                                             */
/*  Author:     F. Georges - fgeorges.org                                   */
/*  Date:       2009-06-14                                                  */
/*  Tags:                                                                   */
/*      Copyright (c) 2009 Florent Georges (see end of file.)               */
/* ------------------------------------------------------------------------ */


package org.expath.pkg.saxon;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.zip.ZipFile;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

/**
 * Represent an EXPath Packaging repository, and provide services on it.
 *
 * @author Florent Georges
 * @date   2009-06-14
 */
public class Repository
{
    public Repository(File repo, UserInteractionStrategy interact)
            throws FileNotFoundException
    {
        if ( repo == null ) {
            throw new NullPointerException("repo is null");
        }
        if ( ! repo.isDirectory() ) {
            throw new FileNotFoundException(repo.getPath());
        }
        myRepo = repo;
        if ( interact == null ) {
            throw new NullPointerException("interact is null");
        }
        myInteract = interact;
    }

    public File getRepoDirectory()
    {
        return myRepo;
    }

    public List<Module> getModules()
            throws PackageException
    {
        if ( myModules == null ) {
            myModules = new ArrayList<Module>();
            ModulesFactory factory = new ModulesFactory(this);
            myModules = factory.read(new File(myRepo, MODULES_FILENAME));
        }
        return myModules;
    }

    public void deleteModule(Module mod)
            throws PackageException
    {
        String msg = "Delete modle " + mod.getTitle() + " (in " + mod.getDirectory().getName() + ")?";
        if ( myInteract.ask(msg, false) ) {
            // invalidate the modules list
            myModules = null;
            // update the <repo>/modules.xml file
            Transformer trans1 = newXsltTransformer(XSLT_DELETE_MOD);
            trans1.setParameter("dir", mod.getDirectory().getName());
            transformXmlFile(trans1, MODULES_FILENAME);
            // update the <repo>/catalog.xml file
            Transformer trans2 = newXsltTransformer(XSLT_DELETE_CAT);
            trans2.setParameter("catalog", mod.getDirectory().getName() + "/catalog.xml");
            transformXmlFile(trans2, "catalog.xml");
            // delete the directory
            deleteDirRecurse(mod.getDirectory());
        }
    }

    private Transformer newXsltTransformer(String style_rsrc)
            throws PackageException
    {
        try {
            ClassLoader loader = Repository.class.getClassLoader();
            InputStream style = loader.getResourceAsStream(style_rsrc);
            TransformerFactory factory = TransformerFactory.newInstance();
            return factory.newTransformer(new StreamSource(style));
        }
        catch ( TransformerException ex ) {
            String msg = "Error compiling an updating stylesheet: " + style_rsrc;
            throw new PackageException(msg, ex);
        }
    }

    private void transformXmlFile(Transformer trans, String name)
            throws PackageException
    {
        try {
            File xml = new File(myRepo, name);
            File copy = new File(myRepo, name + ".COPY");
            if ( copy.exists() ) {
                copy.delete();
            }
            xml.renameTo(copy);
            // to be able to close the stream afterwards
            OutputStream out = new FileOutputStream(xml);
            trans.transform(new StreamSource(copy), new StreamResult(out));
            out.close();
        }
        catch ( TransformerException ex ) {
            throw new PackageException("Error updating the " + name + " file", ex);
        }
        catch ( IOException ex ) {
            throw new PackageException("Error updating the " + name + " file", ex);
        }
    }

    public void installPackage(File pkg, boolean force)
            throws PackageException
    {
        // unzip in temp dir
        File tmp;
        try {
            ZipFile zip = new ZipFile(pkg);
            tmp = ZipHelper.unzipToTmp(zip);
        }
        catch ( IOException ex ) {
            throw new PackageException("Error unziping the package", ex);
        }
        myInteract.logInfo("Package unziped to " + tmp);

        // read the descriptor
        File desc_file = new File(tmp, "expath-pkg.xml");
        PackageDescriptorFactory factory = new PackageDescriptorFactory();
        PackageDescriptor desc = factory.read(desc_file);

        // install each modules
        Iterator<ModuleDescriptor> it = desc.modules();
        while ( it.hasNext() ) {
            ModuleDescriptor m = it.next();
            if ( myInteract.ask("Install module " + m.getTitle() + "?", true) ) {
                String name = m.getName() + "-" + m.getVersion();
                name = myInteract.ask("Install it to dir", name);
                if ( name == null ) {
                    myInteract.logInfo("Install dir is null, abort install.");
                }
                else {
                    File src = new File(tmp, m.getName());
                    File target = new File(myRepo, name);
                    installModule(m, src, target, force);
                }
            }
        }
    }

    private void installModule(ModuleDescriptor m, File src, File target, boolean force)
            throws PackageException
    {
        // preconditions
        assert m != null : "m cannot be null";
        assert src != null : "src cannot be null";
        assert target != null : "target cannot be null";
        if ( target.exists() ) {
            if ( force || myInteract.ask("Force override " + target + "?", false) ) {
                deleteDirRecurse(target);
            }
            else {
                throw new PackageException("Destination directory already exists");
            }
        }
        // invalidate the modules list
        myModules = null;
        // create the dest dir
        target.mkdir();
        // create catalog.xml from package/module/(xsl|xquery)
        File catalog_file = new File(target, "catalog.xml");
        createCatalog(target, catalog_file.getName(), m.stylesheets(), m.getCatalog(), false);
        // create catalog for Java wrappers from package/module/saxon/(xsl|xquery)-wrapper
        File wrappers_file = new File(target, "java-wrappers-cat.xml");
        createCatalog(target, wrappers_file.getName(), m.wrappers(), null, false);
        // create an empty catalog for Java wrappers from package/module/saxon/(xsl|xquery)-wrapper
        File empty_file = new File(target, "empty-wrappers-cat.xml");
        createCatalog(target, empty_file.getName(), m.wrappers(), null, true);
        // copy files
        // TODO: check for catalog.xml, to not override it
        copyDirectory(src, target);
        // handle deps
        Iterator<SaxonDepDescriptor> it = m.dependencies();
        while ( it.hasNext() ) {
            SaxonDepDescriptor dep = it.next();
            // TODO: Handle DEPS (auto + msg for manual) !!!
        }
        // update the <repo>/modules.xml file
        Transformer trans = newXsltTransformer(XSLT_INSTALL_MOD);
        trans.setParameter("name", m.getName());
        trans.setParameter("version", m.getVersion());
        trans.setParameter("dir", target.getName());
        trans.setParameter("title", m.getTitle());
        transformXmlFile(trans, MODULES_FILENAME);
        // update the <repo>/catalog.xml file if needed
        updateMainCatalog(target, catalog_file, m.getCatalogPrefix());
        // update the <repo>/java-wrappers-cat.xml file if needed
        updateMainCatalog(target, wrappers_file, null);
        // update the <repo>/java-wrappers-cat.xml file if needed
        updateMainCatalog(target, empty_file, null);
        // TODO: Update <repo>/std-and-wrappers-cat.xml too? (I mean, will I
        // really create and maintain such a file?)...
        // ...
        // Create the <repo>/<module>/saxon/jar/config.xml
        writeConfig(m, new File(target, "saxon/jar/config.xml"));
    }

    private void updateMainCatalog(File target, File cat, String prefix)
            throws PackageException
    {
        if ( ! cat.exists() ) {
            // If the catalog does not exist, no need to update the main catalog.
            return;
        }
        Transformer trans = newXsltTransformer(XSLT_INSTALL_CAT);
        trans.setParameter("catalog", target.getName() + "/" + cat.getName());
        if ( prefix != null ) {
            // TODO: Catalog delegation does not work properly on Windows with
            // Norm's resolver, because of a bug related to Windows drive
            // letters and URIs schemes...  So if we are on Windows, do not use
            // delegation, just use plain <nextCatalog .../>.
            String os = System.getProperty("os.name");
            if ( os != null && ! os.toLowerCase().contains("windows") ) {
                trans.setParameter("delegate", prefix);
            }
        }
        transformXmlFile(trans, cat.getName());
    }

    private void writeConfig(ModuleDescriptor m, File config_file)
            throws PackageException
    {
        Iterator<String> funs = m.functionClasses();
        Iterator<String> libs = m.libraryClasses();
        if ( ( funs == null || ! funs.hasNext() ) && ( libs == null || ! libs.hasNext() ) ) {
            // If no fun and no lib, just not create the config file.
            return;
        }
        try {
            OutputStream out = new FileOutputStream(config_file);
            writeUtf8(out, "<?xml version=\"1.0\"?>\n\n");
            writeUtf8(out, "<config>\n");
            while ( funs.hasNext() ) {
                writeUtf8(out, "   <function>" + funs.next() + "</function>\n");
            }
            while ( libs.hasNext() ) {
                writeUtf8(out, "   <library>" + libs.next() + "</library>\n");
            }
            writeUtf8(out, "</config>\n");
        }
        catch ( IOException ex ) {
            throw new PackageException("Error writing config.xml", ex);
        }
    }

    private void createCatalog(File module, String cat_name, Iterator<StylesheetDescriptor> stylesheets, byte[] catalog, boolean empty)
            throws PackageException
    {
        // if both are null, there is no catalog to create (module can provide
        // its own directly)
        if ( catalog == null && ( stylesheets == null || ! stylesheets.hasNext() ) ) {
            return;
        }
        try {
            OutputStream out = new FileOutputStream(new File(module, cat_name));
            if ( catalog == null ) {
                generateCatalog(module, stylesheets, out, empty);
            }
            else {
                out.write(catalog);
            }
            out.close();
        }
        catch ( IOException ex ) {
            throw new PackageException("Error writing the catalog", ex);
        }
    }

    private void generateCatalog(File module, Iterator<StylesheetDescriptor> stylesheets, OutputStream out, boolean empty)
            throws IOException
    {
        writeUtf8(out, "<?xml version=\"1.0\"?>\n\n");
        //writeUtf8(out, "<!DOCTYPE catalog PUBLIC\n");
        //writeUtf8(out, " \"-//OASIS//DTD Entity Resolution XML Catalog V1.0//EN\"\n");
        //writeUtf8(out, " \"http://www.oasis-open.org/committees/entity/release/1.0/catalog.dtd\">\n\n");
        writeUtf8(out, "<catalog xmlns=\"urn:oasis:names:tc:entity:xmlns:xml:catalog\">\n\n");
        while ( stylesheets.hasNext() ) {
            StylesheetDescriptor s = stylesheets.next();
            String uri;
            String file;
            if ( s.getImportUri() == null ) {  // xquery
                uri = s.getNamespace();
                if ( empty ) {
                    file = s.getFile() + "-expath-empty-wrapper";
                    createEmptyXQueryWrapper(module, file, s.getNamespace());
                }
                else {
                    file = s.getFile();
                }
            }
            else {                             // xslt
                uri = s.getImportUri();
                file = empty ? "../empty-java-wrapper.xsl" : s.getFile();
            }
            // system element
            writeUtf8(out, "   <system systemId=\"");
            writeUtf8(out, uri);
            writeUtf8(out, "\"\n");
            writeUtf8(out, "           uri=\"");
            writeUtf8(out, file);
            writeUtf8(out, "\"/>\n\n");
            // uri element
            writeUtf8(out, "   <uri name=\"");
            writeUtf8(out, uri);
            writeUtf8(out, "\"\n");
            writeUtf8(out, "        uri=\"");
            writeUtf8(out, file);
            writeUtf8(out, "\"/>\n\n");
        }
        writeUtf8(out, "</catalog>\n");
    }

    /**
     * Create an empty library module.
     *
     * Java extension functions are plugged in XQuery by providing an XQuery
     * library module, the "wrapper", that defines the functions in the correct
     * namespace, the implementation of each calls the Java code by using a
     * "java:" URI scheme namespace.  This is not possible with Saxon HE, so
     * they are then instead provided by registering them to the config object.
     * But then, we have to create an empty library module, with the correct
     * namespace, but empty, so the import statement can be resolved to an
     * actual module...
     */
    private void createEmptyXQueryWrapper(File module, String file, String ns)
            throws IOException
    {
        File f = new File(module, file);
        File d = f.getParentFile();
        if ( d == null ) {
            throw new IOException("Parent is null: " + f);
        }
        if ( ! d.exists() ) {
            if ( ! d.mkdirs() ) {
                throw new IOException("Error creating the directory: " + d);
            }
        }
        OutputStream out = new FileOutputStream(f);
        writeUtf8(out, "module namespace tns = \"");
        writeUtf8(out, ns);
        writeUtf8(out, "\";");
        out.close();
    }

    private void writeUtf8(OutputStream out, String str)
            throws IOException
    {
        out.write(str.getBytes("utf-8"));
    }

    /**
     * Copy {@code src} to {@code target}.
     *
     * Directories are copied recursively.  If {@code target} does not exist, it
     * is created (for both files and dirs.)  If it exist, it is overriden (for
     * files, because dirs are not deleted then recreated, they are just reused.)
     *
     * @param src
     *         The source file or directory.
     * 
     * @param target
     *         The target file or directory (must match the type of {@code src}.)
     */
    public void copyDirectory(File src, File target)
            throws PackageException
    {
        if ( src.isDirectory() ) {
            if ( ! target.exists() ) {
                target.mkdir();
            }
            for ( String child : src.list() ) {
                copyDirectory(new File(src, child), new File(target, child));
            }
        }
        else {
            try {
                InputStream in = new FileInputStream(src);
                OutputStream out = new FileOutputStream(target);
                byte[] buf = new byte[4096];
                int len;
                while ( (len = in.read(buf)) > 0 ) {
                    out.write(buf, 0, len);
                }
                in.close();
                out.close();
            }
            catch ( IOException ex ) {
                throw new PackageException("Error copying " + src + " to " + target, ex);
            }
        }
    }

    private void deleteDirRecurse(File dir)
    {
        File[] children = dir.listFiles();
        if ( children != null ) {
            for ( File child : children ) {
                deleteDirRecurse(child);
            }
        }
        dir.delete();
    }

    private File myRepo;
    private List<Module> myModules;
    private UserInteractionStrategy myInteract;
    private static final String MODULES_FILENAME = "modules.xml";
    private static final String PKG              = "org/expath/pkg/saxon/";
    private static final String XSLT_DELETE_MOD  = PKG + "modules-delete.xsl";
    private static final String XSLT_INSTALL_MOD = PKG + "modules-install.xsl";
    private static final String XSLT_DELETE_CAT  = PKG + "catalog-delete.xsl";
    private static final String XSLT_INSTALL_CAT = PKG + "catalog-install.xsl";
}


/* ------------------------------------------------------------------------ */
/*  DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS COMMENT.               */
/*                                                                          */
/*  The contents of this file are subject to the Mozilla Public License     */
/*  Version 1.0 (the "License"); you may not use this file except in        */
/*  compliance with the License. You may obtain a copy of the License at    */
/*  http://www.mozilla.org/MPL/.                                            */
/*                                                                          */
/*  Software distributed under the License is distributed on an "AS IS"     */
/*  basis, WITHOUT WARRANTY OF ANY KIND, either express or implied.  See    */
/*  the License for the specific language governing rights and limitations  */
/*  under the License.                                                      */
/*                                                                          */
/*  The Original Code is: all this file.                                    */
/*                                                                          */
/*  The Initial Developer of the Original Code is Florent Georges.          */
/*                                                                          */
/*  Contributor(s): none.                                                   */
/* ------------------------------------------------------------------------ */
